Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MSSQLServer module #645

Merged
merged 20 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
- kafka
- localstack
- mongodb
- mssqlserver
- mysql
- nats
- neo4j
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16
v20
36 changes: 36 additions & 0 deletions docs/modules/mssqlserver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# MS SQL Server Module

[Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server) is a relational database management system developed by Microsoft. It provides a platform for efficiently storing, managing, and retrieving structured data. MSSQL offers features for data storage, retrieval, manipulation, and analysis, making it a key component in various applications ranging from small-scale projects to enterprise-level systems.

## Install

```bash
npm install @testcontainers/mssqlserver --save-dev
```

## Examples

!!! warning "EULA Acceptance"
Due to licencing restrictions you are required to accept an EULA for this container image. To indicate that you accept the MS SQL Server image EULA, call the `acceptLicense()` method.

Please see the [`microsoft-mssql-server` image documentation](https://hub.docker.com/_/microsoft-mssql-server#environment-variables) for a link to the EULA document.

<!--codeinclude-->
[Connect and execute query:](../../packages/modules/mssqlserver/src/mssqlserver-container.test.ts) inside_block:connect
<!--/codeinclude-->

<!--codeinclude-->
[Connect and execute query using URI:](../../packages/modules/mssqlserver/src/mssqlserver-container.test.ts) inside_block:uriConnect
<!--/codeinclude-->

<!--codeinclude-->
[Connect and execute query using a valid custom password:](../../packages/modules/mssqlserver/src/mssqlserver-container.test.ts) inside_block:validPassword
<!--/codeinclude-->

<!--codeinclude-->
[Throw error with an invalid password:](../../packages/modules/mssqlserver/src/mssqlserver-container.test.ts) inside_block:invalidPassword
<!--/codeinclude-->

<!--codeinclude-->
[Use a different edition:](../../packages/modules/mssqlserver/src/mssqlserver-container.test.ts) inside_block:expressEdition
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ nav:
- Kafka: modules/kafka.md
- MongoDB: modules/mongodb.md
- MySQL: modules/mysql.md
- MSSQLServer: modules/mssqlserver.md
- Nats: modules/nats.md
- Neo4J: modules/neo4j.md
- PostgreSQL: modules/postgresql.md
Expand Down
8,047 changes: 5,099 additions & 2,948 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/modules/mssqlserver/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Config } from "jest";
import * as path from "path";

const config: Config = {
preset: "ts-jest",
moduleNameMapper: {
"^testcontainers$": path.resolve(__dirname, "../../testcontainers/src"),
},
};

export default config;
40 changes: 40 additions & 0 deletions packages/modules/mssqlserver/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@testcontainers/mssqlserver",
"version": "10.2.1",
"license": "MIT",
"keywords": [
"mssqlserver",
"mssql",
"sqlserver",
"testing",
"docker",
"testcontainers"
],
"description": "MSSQL Server module for Testcontainers",
"homepage": "https://github.com/testcontainers/testcontainers-node#readme",
"repository": {
"type": "git",
"url": "https://github.com/testcontainers/testcontainers-node"
},
"bugs": {
"url": "https://github.com/testcontainers/testcontainers-node/issues"
},
"main": "build/index.js",
"files": [
"build"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"prepack": "shx cp ../../../README.md . && shx cp ../../../LICENSE .",
"build": "tsc --project tsconfig.build.json"
},
"devDependencies": {
"@types/mssql": "^8.1.2",
"mssql": "^10.0.1"
},
"dependencies": {
"testcontainers": "^10.2.1"
}
}
1 change: 1 addition & 0 deletions packages/modules/mssqlserver/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MSSQLServerContainer, StartedMSSQLServerContainer } from "./mssqlserver-container";
102 changes: 102 additions & 0 deletions packages/modules/mssqlserver/src/mssqlserver-container.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import sql, { config } from "mssql";
import { MSSQLServerContainer } from "./mssqlserver-container";

describe("MSSqlServerContainer", () => {
jest.setTimeout(180_000);

// connect {
it("should connect and return a query result", async () => {
const container = await new MSSQLServerContainer().acceptLicense().start();

const sqlConfig: config = {
user: container.getUsername(),
password: container.getPassword(),
database: container.getDatabase(),
server: container.getHost(),
port: container.getPort(),
pool: {
max: 1,
min: 0,
idleTimeoutMillis: 30000,
},
options: {
trustServerCertificate: true,
},
};

const connection = await sql.connect(sqlConfig);

const { recordset } = await connection.query`SELECT 1;`;
expect(recordset).toStrictEqual([{ "": 1 }]);

await connection.close();
await container.stop();
});
// }

// uriConnect {
it("should connect and return a query result with database URI", async () => {
const container = await new MSSQLServerContainer().acceptLicense().start();

const connectionString = container.getConnectionUri();
const connection = await sql.connect(connectionString);

const { recordset } = await connection.query`SELECT 1;`;
expect(recordset).toStrictEqual([{ "": 1 }]);

await connection.close();
await container.stop();
});
// }

// validPassword {
it("should connect and return a query result with valid custom password", async () => {
const container = await new MSSQLServerContainer().acceptLicense().withPassword("I!@M#$eCur3").start();

const connectionString = container.getConnectionUri();
const connection = await sql.connect(connectionString);

const { recordset } = await connection.query`SELECT 1;`;
expect(recordset).toStrictEqual([{ "": 1 }]);

await connection.close();
await container.stop();
});
// }

// invalidPassword {
it("should throw error with invalid password", async () => {
const container = new MSSQLServerContainer().acceptLicense().withPassword("password");
await expect(container.start()).rejects.toThrow(
Error('Log stream ended and message "/.*Recovery is complete.*/" was not received')
);
});
// }

// expressEdition {
it("should start db with express edition", async () => {
const container = await new MSSQLServerContainer()
.withWaitForMessage(/.*Attribute synchronization manager initialized*/)
.acceptLicense()
.withEnvironment({ MSSQL_PID: "Express" })
.start();

const { output, exitCode } = await container.exec([
"/opt/mssql-tools/bin/sqlcmd",
"-S",
container.getHost(),
"-U",
container.getUsername(),
"-P",
container.getPassword(),
"-Q",
"SELECT @@VERSION;",
]);

expect(exitCode).toBe(0);
expect(output).toContain("Express Edition");

await container.stop();
});
// }
});
82 changes: 82 additions & 0 deletions packages/modules/mssqlserver/src/mssqlserver-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";

const MSSQL_PORT = 1433;
export class MSSQLServerContainer extends GenericContainer {
private database = "master";
private username = "sa";
private password = "Passw0rd";
private acceptEula = "N";
private message: string | RegExp = /.*Recovery is complete.*/;

constructor(image = "mcr.microsoft.com/mssql/server:2022-latest") {
super(image);
}

public acceptLicense(): this {
this.acceptEula = "Y";
return this;
}

public withDatabase(database: string): this {
this.database = database;
return this;
}
Comment on lines +20 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, you can not set the database name in MSSQL and due to there is no test covering the case I wonder if that works, nowadays.

Copy link
Contributor Author

@wjin17 wjin17 Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this in as a convenience method for the generating the connection string should the user want to seed the instance during initialization. I can also add a test/example for seeding the database with SQL files and bash scripts but I was worried about adding too many files/adding things out of scope. Let me know how I should proceed.


public withPassword(password: string): this {
this.password = password;
return this;
}

public withWaitForMessage(message: string | RegExp): this {
this.message = message;
return this;
}

public override async start(): Promise<StartedMSSQLServerContainer> {
this.withExposedPorts(...(this.hasExposedPorts ? this.exposedPorts : [MSSQL_PORT]))
.withEnvironment({
ACCEPT_EULA: this.acceptEula,
MSSQL_SA_PASSWORD: this.password,
MSSQL_TCP_PORT: String(MSSQL_PORT),
})
.withWaitStrategy(Wait.forLogMessage(this.message, 1))
.withStartupTimeout(120_000);

return new StartedMSSQLServerContainer(await super.start(), this.database, this.username, this.password);
}
}

export class StartedMSSQLServerContainer extends AbstractStartedContainer {
constructor(
startedTestContainer: StartedTestContainer,
private readonly database: string,
private readonly username: string,
private readonly password: string
) {
super(startedTestContainer);
}

public getPort(): number {
return this.getMappedPort(MSSQL_PORT);
}

public getDatabase(): string {
return this.database;
}

public getUsername(): string {
return this.username;
}

public getPassword(): string {
return this.password;
}

/**
* @param {boolean} secure use secure connection?
* @returns A connection URI in the form of `Server=<host>,1433;Database=<database>;User Id=<username>;Password=<password>;Encrypt=false`
*/
public getConnectionUri(secure = false): string {
return `Server=${this.getHost()},${this.getPort()};Database=${this.getDatabase()};User Id=${this.getUsername()};Password=${this.getPassword()};Encrypt=${secure}`;
}
}
9 changes: 9 additions & 0 deletions packages/modules/mssqlserver/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"exclude": ["build", "jest.config.ts", "src/**/*.test.ts"],
"references": [
{
"path": "../../testcontainers"
}
]
}
16 changes: 16 additions & 0 deletions packages/modules/mssqlserver/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build",
"paths": {
"testcontainers": ["../../testcontainers/src"]
}
},
"exclude": ["build", "jest.config.ts"],
"references": [
{
"path": "../../testcontainers"
}
]
}
Loading